Imports System.Diagnostics
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.IO
Imports System.Linq
Imports System.Net
Imports System.Threading.Tasks
Imports BruTile
Imports BruTile.Cache
Imports BruTile.PreDefined
Imports BruTile.Web
Imports DotSpatial.Projections
Imports DotSpatial.Topology
Imports Exception = System.Exception
Imports Point = DotSpatial.Topology.Point

Friend Class TileManager
#Region "Private"

    Private _tileCache As TileCache
    Private _tileServerName As String
    Private _tileServerUrl As String

#End Region

#Region "Properties"

    Public ReadOnly Property TileServerURL() As String
        Get
            Return _tileServerUrl
        End Get
    End Property

    Public ReadOnly Property TileServerName() As String
        Get
            Return _tileServerName
        End Get
    End Property

    Public Property WmsServerInfo() As WmsServerInfo
        Get
            Return m_WmsServerInfo
        End Get
        Set(value As WmsServerInfo)
            m_WmsServerInfo = Value
        End Set
    End Property
    Private m_WmsServerInfo As WmsServerInfo

#End Region

    Public ShowErrorInTile As Boolean = True

    ''' <summary>
    '''
    ''' </summary>
    Public Sub New()
    End Sub

    ''' <summary>
    '''
    ''' </summary>
    ''' <param name="tileServerName"></param>
    ''' <param name="tileServerUrl"></param>
    Public Sub New(tileServerName As String, tileServerUrl As String)
        _tileServerUrl = tileServerUrl
        _tileServerName = tileServerName

        _tileCache = New TileCache(_tileServerName)
    End Sub

    Public Property TileSource() As ITileSource
        Get
            Return m_TileSource
        End Get
        Private Set(value As ITileSource)
            m_TileSource = Value
        End Set
    End Property
    Private m_TileSource As ITileSource

    Public Property TileCache() As ITileCache(Of Byte())
        Get
            Return m_TileCache
        End Get
        Private Set(value As ITileCache(Of Byte()))
            m_TileCache = Value
        End Set
    End Property
    Private m_TileCache As ITileCache(Of Byte())

    ''' <summary>
    '''
    ''' </summary>
    ''' <param name="tileServerName"></param>
    ''' <param name="tileServerUrl"></param>
    Public Sub ChangeService(tileServerName As String, tileServerUrl As String)
        _tileServerUrl = tileServerUrl
        _tileServerName = tileServerName

        _tileCache = New TileCache(_tileServerName)
    End Sub

    ''' <summary>
    '''
    ''' </summary>
    ''' <param name="envelope"></param>
    ''' <param name="bounds"></param>
    ''' <returns></returns>
    Public Function GetTiles(envelope As Envelope, bounds As Rectangle) As Tile(,)
        Dim mapTopLeft As Coordinate = envelope.TopLeft()
        Dim mapBottomRight As Coordinate = envelope.BottomRight()

        'Clip the coordinates so they are in the range of the web mercator projection
        mapTopLeft.Y = TileCalculator.Clip(mapTopLeft.Y, TileCalculator.MinLatitude, TileCalculator.MaxLatitude)
        mapTopLeft.X = TileCalculator.Clip(mapTopLeft.X, TileCalculator.MinLongitude, TileCalculator.MaxLongitude)

        mapBottomRight.Y = TileCalculator.Clip(mapBottomRight.Y, TileCalculator.MinLatitude, TileCalculator.MaxLatitude)
        mapBottomRight.X = TileCalculator.Clip(mapBottomRight.X, TileCalculator.MinLongitude, TileCalculator.MaxLongitude)

        Dim zoom As Integer = TileCalculator.DetermineZoomLevel(envelope, bounds)

        Dim topLeftTileXY As Point = TileCalculator.LatLongToTileXY(mapTopLeft, zoom)
        Dim btmRightTileXY As Point = TileCalculator.LatLongToTileXY(mapBottomRight, zoom)

        Dim tileMatrix = New Tile(CInt(btmRightTileXY.X - topLeftTileXY.X), CInt(btmRightTileXY.Y - topLeftTileXY.Y)) {}

        Parallel.[For](CInt(topLeftTileXY.Y), CInt(btmRightTileXY.Y) + 1,
                       Function(y) Parallel.[For](CInt(topLeftTileXY.X), CInt(btmRightTileXY.X) + 1,
                                                Sub(x)
                                                    Dim currTopLeftPixXY = TileCalculator.TileXYToTopLeftPixelXY(x, y)
                                                    Dim currTopLeftCoord = TileCalculator.PixelXYToLatLong(CInt(currTopLeftPixXY.X), CInt(currTopLeftPixXY.Y), zoom)

                                                    Dim currBtmRightPixXY = TileCalculator.TileXYToBottomRightPixelXY(x, y)
                                                    Dim currBtmRightCoord = TileCalculator.PixelXYToLatLong(CInt(currBtmRightPixXY.X), CInt(currBtmRightPixXY.Y), zoom)

                                                    Dim currEnv = New Envelope(currTopLeftCoord, currBtmRightCoord)

                                                    Dim tile = GetTile(x, y, currEnv, zoom)
                                                    tileMatrix(x - CInt(topLeftTileXY.X), y - CInt(topLeftTileXY.Y)) = tile
                                                End Sub))
        Return tileMatrix
    End Function

    ''' <summary>
    '''
    ''' </summary>
    ''' <param name="x"></param>
    ''' <param name="y"></param>
    ''' <param name="envelope"></param>
    ''' <param name="zoom"></param>
    ''' <returns></returns>
    <System.Diagnostics.DebuggerStepThrough()>
    Public Function GetTile(x As Integer, y As Integer, envelope As Envelope, zoom As Integer) As Tile
        Dim bitmap As Bitmap = _tileCache.[Get](zoom, x, y)
        If bitmap Is Nothing Then
            bitmap = GetViaBrutile(x, y, zoom, envelope)
        End If
        If bitmap IsNot Nothing Then
            Dim tile = New Tile(x, y, zoom, envelope, bitmap)
            Return tile
        End If
        Try
            Dim url As String = _tileServerUrl

            If url.Contains("{key}") Then
                Dim quadKey As String = TileCalculator.TileXYToBingQuadKey(x, y, zoom)
                url = url.Replace("{key}", quadKey)
            Else
                url = url.Replace("{zoom}", zoom.ToString())
                url = url.Replace("{x}", x.ToString())
                url = url.Replace("{y}", y.ToString())
            End If

            Dim client = New WebClient()
            Dim stream = client.OpenRead(url)

            If stream IsNot Nothing Then
                bitmap = New Bitmap(stream)
            End If

            Dim tile = New Tile(x, y, zoom, envelope, bitmap)

            If stream IsNot Nothing Then
                stream.Flush()
                stream.Close()
            End If

            'Put the tile in the cache
            _tileCache.Put(tile)

            Return tile
        Catch ex As Exception
            ' We may see a 400 (Bad Request) when the user is zoomed in too far.
            Debug.WriteLine(ex.Message)

            'Return a No Data Available tile
            Dim noDataTile = New Tile(x, y, zoom, envelope, My.Resources.NoDataTile)

            Return noDataTile
        End Try
    End Function

    ''' <summary>
    '''
    ''' </summary>
    ''' <param name="point"></param>
    ''' <param name="envelope"></param>
    ''' <param name="zoom"></param>
    ''' <returns></returns>
    Public Function GetTile(point As Point, envelope As Envelope, zoom As Integer) As Tile
        Return GetTile(CInt(point.X), CInt(point.Y), envelope, zoom)
    End Function

    ''' <summary>
    '''
    ''' </summary>
    ''' <param name="coord"></param>
    ''' <param name="zoom"></param>
    ''' <returns></returns>
    Public Function GetTileFromLatLong(coord As Coordinate, zoom As Integer) As Tile
        Dim tileXY = TileCalculator.LatLongToTileXY(coord, zoom)

        'Figure out the extent of the tile so that it can be made into MWImageData
        Dim tileTopLeftXY = TileCalculator.TileXYToTopLeftPixelXY(CInt(tileXY.X), CInt(tileXY.Y))
        Dim tileBottomRightXY = TileCalculator.TileXYToTopLeftPixelXY(CInt(tileXY.X) + 1, CInt(tileXY.Y) + 1)

        Dim tileTopLeft = TileCalculator.PixelXYToLatLong(CInt(tileTopLeftXY.X), CInt(tileTopLeftXY.Y), zoom)
        Dim tileBottomRight = TileCalculator.PixelXYToLatLong(CInt(tileBottomRightXY.X), CInt(tileBottomRightXY.Y), zoom)

        Dim envelope = New Envelope(tileTopLeft, tileBottomRight)

        Return GetTile(tileXY, envelope, zoom)
    End Function

    <System.Diagnostics.DebuggerStepThrough()>
    Private Function GetViaBrutile(x As Integer, y As Integer, zoom As Integer, envelope As IEnvelope) As Bitmap
        If Not InitializeBrutileProvider() Then
            Return Nothing
        End If

        Dim extent = ToBrutileExtent(New Global.DotSpatial.Data.Extent(envelope))
        Dim tiles = TileSource.Schema.GetTilesInView(extent, zoom).ToList()
        'Debug.Assert(tiles.Count() == 1);

        Dim tileInfo = tiles(0)
        tileInfo.Index = New TileIndex(x, y, zoom)

        Try
            Dim bytes As Byte() = TileSource.Provider.GetTile(tileInfo)
            Return New Bitmap(New MemoryStream(bytes))
        Catch ex As WebException
            If ShowErrorInTile Then
                'hack: an issue with this method is that one an error tile is in the memory cache it will stay even
                'if the error is resolved. PDD.
                Using bitmap = New Bitmap(TileSource.Schema.Width, TileSource.Schema.Height)
                    Using graphics__1 = Graphics.FromImage(bitmap)
                        graphics__1.DrawString(ex.Message, New Font(FontFamily.GenericSansSerif, 12), New SolidBrush(Color.Black), New RectangleF(0, 0, TileSource.Schema.Width, TileSource.Schema.Height))
                    End Using

                    Using m = New MemoryStream()
                        bitmap.Save(m, ImageFormat.Png)
                        Return New Bitmap(m)
                    End Using
                End Using
            End If
            Debug.WriteLine(ex.Message)
        Catch ex As Exception
            'todo: log and use other ways to report to user.
            Debug.WriteLine(ex.Message)
        End Try
        Return Nothing
    End Function

    Private Function InitializeBrutileProvider() As Boolean
        If Me.TileServerName.Equals(My.Resources.BingHybrid, StringComparison.InvariantCultureIgnoreCase) Then
            Dim token As String = [String].Empty
            Dim url As String = If(String.IsNullOrWhiteSpace(token), BingRequest.UrlBingStaging, BingRequest.UrlBing)

            TileSource = New BingTileSource(New BingRequest(url, String.Empty, BingMapType.Hybrid))
            TileCache = New MemoryCache(Of Byte())(100, 200)

            Return True
        End If

        If Me.TileServerName.Equals(My.Resources.GoogleSatellite, StringComparison.InvariantCultureIgnoreCase) Then
            TileSource = New GoogleTileSource(GoogleMapType.GoogleSatellite)
            TileCache = New MemoryCache(Of Byte())(100, 200)

            Return True
        End If

        If Me.TileServerName.Equals(My.Resources.GoogleMap, StringComparison.InvariantCultureIgnoreCase) Then
            TileSource = New GoogleTileSource(GoogleMapType.GoogleMap)
            TileCache = New MemoryCache(Of Byte())(100, 200)

            Return True
        End If

        '
        '                TileSource = new YahooTileSource(YahooMapType.YahooMap);
        '                TileCache = new MemoryCache<byte[]>(100, 200);                
        '                return true;
        '                 

        If Me.TileServerName.Equals(My.Resources.YahooMap, StringComparison.InvariantCultureIgnoreCase) Then
        End If

        '
        '                TileSource = new YahooTileSource(YahooMapType.YahooSatellite);
        '                TileCache = new MemoryCache<byte[]>(100, 200);                
        '                return true;
        '                 

        If Me.TileServerName.Equals(My.Resources.YahooSatellite, StringComparison.InvariantCultureIgnoreCase) Then
        End If

        If Me.TileServerName.Equals(My.Resources.WMSMap, StringComparison.InvariantCultureIgnoreCase) Then
            TileSource = WmsTileSource.Create(WmsServerInfo)
            TileCache = New MemoryCache(Of Byte())(100, 200)

            Return True
        End If

        TileSource = Nothing
        TileCache = Nothing

        Return False
    End Function

    Private Shared Function ToBrutileExtent(extent As Global.DotSpatial.Data.Extent) As Extent
        Return New Extent(extent.MinX, extent.MinY, extent.MaxX, extent.MaxY)
    End Function
End Class
